<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>流式输出打字机效果演示</title>
    <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='75' font-size='75' fill='%23667eea'>✨</text></svg>">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }

        .container {
            width: 100%;
            max-width: 900px;
        }

        .header {
            text-align: center;
            color: white;
            margin-bottom: 30px;
        }

        .header h1 {
            font-size: 2.5em;
            margin-bottom: 10px;
            text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
        }

        .header p {
            font-size: 1.1em;
            opacity: 0.9;
        }

        .card {
            background: white;
            border-radius: 12px;
            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
            padding: 30px;
            overflow: hidden;
        }

        .controls {
            margin-bottom: 25px;
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }

        .controls label {
            display: flex;
            flex-direction: column;
            gap: 5px;
            flex: 1;
            min-width: 250px;
        }

        .controls label span {
            font-size: 0.9em;
            color: #666;
            font-weight: 500;
        }

        .controls input[type="text"] {
            padding: 10px 15px;
            border: 2px solid #e0e0e0;
            border-radius: 6px;
            font-size: 1em;
            transition: all 0.3s ease;
        }

        .controls input[type="text"]:focus {
            outline: none;
            border-color: #667eea;
            box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
        }

        .button-group {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }

        button {
            padding: 10px 20px;
            border: none;
            border-radius: 6px;
            font-size: 1em;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .btn-start {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            flex: 1;
            min-width: 120px;
            justify-content: center;
        }

        .btn-start:hover:not(:disabled) {
            transform: translateY(-2px);
            box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
        }

        .btn-start:disabled {
            opacity: 0.6;
            cursor: not-allowed;
        }

        .btn-clear {
            background: #f0f0f0;
            color: #333;
            flex: 1;
            min-width: 100px;
            justify-content: center;
        }

        .btn-clear:hover {
            background: #e0e0e0;
        }

        .status {
            margin-bottom: 15px;
            padding: 12px 15px;
            border-radius: 6px;
            font-size: 0.95em;
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .status.idle {
            background: #f0f0f0;
            color: #666;
        }

        .status.loading {
            background: #e3f2fd;
            color: #1976d2;
        }

        .status.done {
            background: #e8f5e9;
            color: #388e3c;
        }

        .status.error {
            background: #ffebee;
            color: #d32f2f;
        }

        .status::before {
            content: '';
            display: inline-block;
            width: 10px;
            height: 10px;
            border-radius: 50%;
            flex-shrink: 0;
        }

        .status.idle::before {
            background: #999;
        }

        .status.loading::before {
            background: #1976d2;
            animation: pulse 1.5s ease-in-out infinite;
        }

        .status.done::before {
            background: #388e3c;
        }

        .status.error::before {
            background: #d32f2f;
        }

        @keyframes pulse {
            0%, 100% {
                opacity: 1;
            }
            50% {
                opacity: 0.5;
            }
        }

        .output-container {
            position: relative;
            background: #fafafa;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
            padding: 20px;
            min-height: 200px;
            max-height: 400px;
            overflow-y: auto;
            font-size: 1.1em;
            line-height: 1.8;
            word-break: break-word;
            white-space: pre-wrap;
            color: #333;
            font-family: 'Courier New', monospace;
        }

        .output-container::-webkit-scrollbar {
            width: 8px;
        }

        .output-container::-webkit-scrollbar-track {
            background: #f0f0f0;
            border-radius: 4px;
        }

        .output-container::-webkit-scrollbar-thumb {
            background: #667eea;
            border-radius: 4px;
        }

        .output-container::-webkit-scrollbar-thumb:hover {
            background: #764ba2;
        }

        .cursor {
            display: inline-block;
            width: 2px;
            height: 1.2em;
            background: #667eea;
            margin-left: 2px;
            animation: blink 1s ease-in-out infinite;
            vertical-align: text-bottom;
        }

        @keyframes blink {
            0%, 49% {
                opacity: 1;
            }
            50%, 100% {
                opacity: 0;
            }
        }

        .stats {
            margin-top: 15px;
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
            gap: 10px;
            font-size: 0.9em;
            color: #666;
        }

        .stat-item {
            background: #f5f5f5;
            padding: 10px 15px;
            border-radius: 6px;
            border-left: 4px solid #667eea;
        }

        .stat-item strong {
            color: #333;
            display: block;
            margin-bottom: 3px;
        }

        .emoji-burst {
            position: fixed;
            font-size: 2em;
            pointer-events: none;
            animation: burstAnimation 1.5s ease-out forwards;
        }

        @keyframes burstAnimation {
            0% {
                opacity: 1;
                transform: translate(0, 0) scale(1);
            }
            100% {
                opacity: 0;
                transform: translate(var(--tx), var(--ty)) scale(0);
            }
        }

        @media (max-width: 768px) {
            .header h1 {
                font-size: 1.8em;
            }

            .controls {
                flex-direction: column;
            }

            .controls label {
                min-width: 100%;
            }

            .card {
                padding: 20px;
            }
        }
    </style>
</head>
<body>
<div class="container">
    <div class="header">
        <h1>✨ 流式输出演示</h1>
        <p>实时流式接收和打字机效果展现</p>
    </div>

    <div class="card">
        <div class="controls">
            <label>
                <span>自定义输出内容（可选）</span>
                <input type="text" id="customContent" placeholder="不输入则使用默认内容">
            </label>
            <div class="button-group" style="width: 100%;">
                <button class="btn-start" id="startBtn" onclick="startStream()">
                    <span>▶️</span> 开始流式输出
                </button>
                <button class="btn-clear" onclick="clearOutput()">🗑️ 清空</button>
            </div>
        </div>

        <div id="status" class="status idle">未启动</div>

        <div class="output-container" id="output"></div>

        <div class="stats">
            <div class="stat-item">
                <strong>字符数</strong>
                <span id="charCount">0</span>
            </div>
            <div class="stat-item">
                <strong>耗时</strong>
                <span id="duration">0ms</span>
            </div>
            <div class="stat-item">
                <strong>状态</strong>
                <span id="streamStatus">就绪</span>
            </div>
        </div>
    </div>
</div>

<script>
    let startTime = null;
    let charCount = 0;
    let isStreaming = false;

    function startStream() {
        const customContent = document.getElementById('customContent').value;
        const url = customContent
            ? `/test/sseStream?content=${encodeURIComponent(customContent)}`
            : '/test/sseStream';

        clearOutput();
        isStreaming = true;
        charCount = 0;
        startTime = Date.now();
        document.getElementById('startBtn').disabled = true;
        document.getElementById('customContent').disabled = true;

        updateStatus('loading', '流式连接中...');
        document.getElementById('streamStatus').textContent = '连接中';

        const eventSource = new EventSource(url);
        const output = document.getElementById('output');

        eventSource.addEventListener('start', (event) => {
            updateStatus('loading', '开始接收数据...');
            document.getElementById('streamStatus').textContent = '接收中';
            output.innerHTML = '';
            createEmojiBurst();
        });

        eventSource.addEventListener('message', (event) => {
            const data = event.data;
            output.innerHTML += data;
            charCount++;
            document.getElementById('charCount').textContent = charCount;
            updateDuration();

            // 自动滚动到底部
            output.parentElement.scrollTop = output.parentElement.scrollHeight;
        });

        eventSource.addEventListener('done', (event) => {
            updateStatus('done', '流式输出完成！');
            document.getElementById('streamStatus').textContent = '完成';
            isStreaming = false;
            document.getElementById('startBtn').disabled = false;
            document.getElementById('customContent').disabled = false;

            // 移除光标
            const cursor = document.querySelector('.cursor');
            if (cursor) cursor.remove();

            // 爆炸效果
            createExplosion();
            eventSource.close();
        });

        eventSource.addEventListener('error', (event) => {
            if (event.eventPhase === EventSource.CLOSED) {
                console.log('连接已关闭');
            } else {
                updateStatus('error', '连接出错: ' + event.type);
                document.getElementById('streamStatus').textContent = '错误';
                isStreaming = false;
                document.getElementById('startBtn').disabled = false;
                document.getElementById('customContent').disabled = false;
            }
            eventSource.close();
        });
    }

    function updateStatus(type, message) {
        const statusEl = document.getElementById('status');
        statusEl.className = 'status ' + type;
        statusEl.textContent = message;
    }

    function updateDuration() {
        if (startTime) {
            const duration = Date.now() - startTime;
            document.getElementById('duration').textContent = duration + 'ms';
        }
    }

    function clearOutput() {
        document.getElementById('output').innerHTML = '';
        charCount = 0;
        document.getElementById('charCount').textContent = '0';
        document.getElementById('duration').textContent = '0ms';
        document.getElementById('streamStatus').textContent = '就绪';
        updateStatus('idle', '未启动');
    }

    function createEmojiBurst() {
        const emojis = ['✨', '🎉', '🚀', '⭐', '💫', '🌟'];
        for (let i = 0; i < 8; i++) {
            const emoji = document.createElement('div');
            emoji.className = 'emoji-burst';
            emoji.textContent = emojis[Math.floor(Math.random() * emojis.length)];
            emoji.style.left = Math.random() * window.innerWidth + 'px';
            emoji.style.top = Math.random() * window.innerHeight + 'px';
            emoji.style.setProperty('--tx', (Math.random() - 0.5) * 200 + 'px');
            emoji.style.setProperty('--ty', (Math.random() - 0.5) * 200 + 'px');
            document.body.appendChild(emoji);

            setTimeout(() => emoji.remove(), 1500);
        }
    }

    function createExplosion() {
        const output = document.getElementById('output');
        const rect = output.getBoundingClientRect();

        // 创建多个爆炸效果的数字
        const explosionCount = 15;
        for (let i = 0; i < explosionCount; i++) {
            const digit = document.createElement('div');
            digit.className = 'emoji-burst';
            digit.textContent = Math.floor(Math.random() * 10);
            digit.style.fontSize = '1.5em';
            digit.style.fontWeight = 'bold';
            digit.style.color = '#' + Math.floor(Math.random()*16777215).toString(16);

            const centerX = rect.left + rect.width / 2;
            const centerY = rect.top + rect.height / 2;
            digit.style.left = centerX + 'px';
            digit.style.top = centerY + 'px';

            // 计算爆炸的方向和距离
            const angle = (i / explosionCount) * Math.PI * 2;
            const distance = 150 + Math.random() * 100;
            const tx = Math.cos(angle) * distance;
            const ty = Math.sin(angle) * distance;

            digit.style.setProperty('--tx', tx + 'px');
            digit.style.setProperty('--ty', ty + 'px');
            document.body.appendChild(digit);

            setTimeout(() => digit.remove(), 1500);
        }

        // 额外的闪光效果
        createEmojiBurst();
    }

    // 页面加载完成后的初始化
    window.addEventListener('load', () => {
        clearOutput();
    });

    // 防止未保存时关闭
    document.getElementById('output').addEventListener('click', function(e) {
        // 使内容可以被选中复制
        if (e.target === this) {
            this.focus();
        }
    });
</script>
</body>
</html>